/*
 * This file is part of the coreboot project.
 *
 * Copyright 2014 Google Inc.
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; version 2 of the License.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA, 02110-1301 USA
 */

#include <string.h>
#include <stdlib.h>
#include <arch/cpu.h>
#include <arch/smc.h>
#include <arch/exception.h>
#include <arch/lib_helpers.h>
#include <console/console.h>

enum {
	/* SMC called from AARCH32 */
	EC_SMC_AARCH32 = 0x13,
	/* SMC called from AARCH64 */
	EC_SMC_AARCH64 = 0x17,

	SMC_NUM_RANGES = 8,
};

struct smc_range {
	uint32_t func_begin;
	uint32_t func_end;
	int (*handler)(struct smc_call *);
};

struct smc_ranges {
	size_t used;
	struct smc_range handlers[SMC_NUM_RANGES];
};

static struct smc_ranges smc_functions;

static struct smc_range *smc_handler_by_function(uint32_t fid)
{
	int i;

	for (i = 0; i < smc_functions.used; i++) {
		struct smc_range *r = &smc_functions.handlers[i];

		if (fid >= r->func_begin && fid <= r->func_end)
			return r;
	}

	return NULL;
}

int smc_register_range(uint32_t min, uint32_t max, int (*h)(struct smc_call *))
{
	struct smc_range *r;

	if (smc_functions.used == SMC_NUM_RANGES)
		return -1;

	if (min > max)
		return -1;

	/* This check isn't exhaustive but it's fairly quick. */
	if (smc_handler_by_function(min) || smc_handler_by_function(max))
		return -1;

	r = &smc_functions.handlers[smc_functions.used];
	r->func_begin = min;
	r->func_end = max;
	r->handler = h;
	smc_functions.used++;

	return 0;
}

static int smc_cleanup(struct exc_state *state, struct smc_call *smc, int ret)
{
	memcpy(&state->regs.x, &smc->results, sizeof(smc->results));

	return ret;
}

static int smc_return_with_error(struct exc_state *state, struct smc_call *smc)
{
	smc32_return(smc, SMC_UNKNOWN_FUNC);
	return smc_cleanup(state, smc, EXC_RET_HANDLED);
}

static int smc_handler(struct exc_state *state, uint64_t vector_id)
{
	struct smc_call smc_storage;
	struct smc_call *smc = &smc_storage;
	uint32_t exception_class;
	uint32_t esr;
	struct smc_range *r;

	memcpy(&smc->args, &state->regs.x, sizeof(smc->args));
	memcpy(&smc->results, &state->regs.x, sizeof(smc->results));

	esr = raw_read_esr_el3();
	exception_class = (esr >> 26) & 0x3f;

	/* No support for SMC calls from AARCH32 */
	if (exception_class == EC_SMC_AARCH32)
		return smc_return_with_error(state, smc);

	/* Check to ensure this is an SMC from AARCH64. */
	if (exception_class != EC_SMC_AARCH64)
		return EXC_RET_IGNORED;

	/* Ensure immediate value is 0. */
	if ((esr & 0xffff) != 0)
		return smc_return_with_error(state, smc);

	r = smc_handler_by_function(smc_function_id(smc));

	if (r != NULL) {
		if (!r->handler(smc))
			return smc_cleanup(state, smc, EXC_RET_HANDLED);
	}

	return smc_return_with_error(state, smc);
}

/* SMC calls can be generated by 32-bit or 64-bit code. */
static struct exception_handler smc_handler64 = {
	.handler = &smc_handler,
};

static struct exception_handler smc_handler32 = {
	.handler = &smc_handler,
};

static void enable_smc(void *arg)
{
	uint32_t scr;

	/* Enable SMC */
	scr = raw_read_scr_el3();
	scr &= ~(SCR_SMC_MASK);
	scr |= SCR_SMC_ENABLE;
	raw_write_scr_el3(scr);
}

void smc_init(void)
{
	struct cpu_action action = {
		.run = enable_smc,
	};

	arch_run_on_all_cpus_async(&action);

	/* Register SMC handlers. */
	exception_handler_register(EXC_VID_LOW64_SYNC, &smc_handler64);
	exception_handler_register(EXC_VID_LOW32_SYNC, &smc_handler32);
}
